/**
 *  Module dependencies.
 */
var fs   = require('fs')
  , debug = require('debug')('koa-liveload')
  , path = require('path');

/**
 * Utility functions to synchronously test whether the giving path
 * is a file or a directory.
 */
var is = (function(ret) {
  ['file', 'dir'].forEach(function(method) {
    ret[method] = function(fpath) {
      var suffix = ({file: 'File', dir: 'Directory'})[method];
      if (fs.existsSync(fpath)) {
        return fs.statSync(fpath)['is' + suffix]();
      }
      return false;
    }
  });
  return ret;
}({}));


/**
 *  Get sub-directories in a directory.
 */
var sub = function(parent, cb) {
  if (is.dir(parent)) {
    fs.readdir(parent, function(err, all) {
      all && all.forEach(function(f) {
        var sdir = path.join(parent, f)
        if (is.dir(sdir)) {
          cb.call(null, sdir)
        }
      });
    });
  }
};


/**
 * A container for memorizing names of files or directories.
 */
var memo = (function(memo) {
  return {
    push: function(name, type) {
      memo[name] = type;
    },
    has: function(name) {
      return memo.hasOwnProperty(name) ? true : false;
    },
    update: function(name) {
      if (!is.file(name) && !is.dir(name)) {
        delete memo[name];
      }
      return true;
    }
  };
}({}));


/**
 *  A Container for storing unique and valid filenames.
 */
var fileNameCache = (function(cache) {
  return {
    push: function(name) {
      cache[name] = 1;
      return this;
    },
    each: function() {
      var temp = Object.keys(cache).filter(function(name){
        if (memo.has(name)) {
          memo.update(name);
        }
        return true;
      });
      temp.forEach.apply(temp, arguments);
      return this;
    },
    clear: function(){
      cache = {};
      return this;
    }
  };
}({}));


/**
 * Abstracting the way of avoiding duplicate function call.
 */
var worker = (function() {
  var free = true;
  return {
    busydoing: function(cb) {
      if (free) {
        free = false;
        cb.call();
      }
    },
    free: function() {
      free = true;
    }
  }
}());



/**
 * Watch a file or a directory recursively.
 *
 * @param {String} fpath
 * @param {Object} options includes ['html', 'js', 'css'] excludes ['node_modules', 'components']
 * @param {Function} cb
 *
 *   watch('fpath', function(file) {
 *     console.log(file, ' changed');
 *   });
 */

function watchDir(fpath, opts, cb){
  if(typeof(opts) == "function"){
    cb = opts;
    opts = {};
  }
  opts.includes = opts.includes || ['js'];
  opts.excludes = opts.excludes || ['node_modules'];
  opts.ignoreHidden = opts.ignoreHidden || true;
  watch(fpath, cb);

  var normalizeCall = function(fname, cb) {
    debug('changed file: ' + fname);
    // Store each name of the modifying or temporary files generated by an editor.
    fileNameCache.push(fname);

    worker.busydoing(function() {
      // A heuristic delay of the write-to-file process.
      setTimeout(function() {

        // When the write-to-file process is done, send all filtered filenames
        // to the callback function and call it.
        fileNameCache
          .each(function(f) {
            // Watch new created directory.
            if (!memo.has(f) && is.dir(f)) {
              watch(f, cb);
            } else if (is.file(f)) {
              cb.call(null, f);
            }
          }).clear();

        worker.free();

      }, 100);
    })
  }

  function watch(fpath, cb) {
    var basename = path.basename(fpath);
    if(!is.dir(fpath)) return;
    if(opts.ignoreHidden && /^\./.test(basename)) return;
    if(opts.excludes && opts.excludes.indexOf(basename) !== -1) return;

    debug('watching directory: ' + fpath);
    memo.push(fpath, 'dir');
    fs.watch(fpath, function(err, fname) {
      // Windows "delete" operations do not pass the filename that was deleted
      if(!fname) return;
      var ext = path.extname(fname).slice(1);
      var f = path.join(fpath, fname);
      if (is.file(f) && opts.includes.indexOf(ext) === -1) return;
      normalizeCall(f, cb);
    });
    // Recursively watch its sub-directories.
    sub(fpath, function(dir) {
      watch(dir, cb);
    });
  }
}


// Expose.
module.exports = watchDir;